1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.font.ttf; 12 import hip.api.data.font; 13 14 immutable dstring defaultCharset = " \náéíóúãñçabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\\|'\"`/*-+,.;:´_=!@#$%&()[]{}~^?"; 15 16 version = HipArsdFont; 17 18 19 private uint nextPowerOfTwo(uint number) 20 { 21 ulong value = 1; 22 while(value < number) 23 { 24 value <<= 1; 25 } 26 return cast(uint)value; 27 } 28 29 private int round(float f) 30 { 31 return cast(int)(f+0.5); 32 } 33 34 class HipNullFont : HipFont 35 { 36 string path; 37 this(){} 38 this(string path, uint fontSize = 32) 39 { 40 } 41 /** 42 * This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used 43 * If you don't care about async 44 */ 45 bool loadFromMemory(in ubyte[] data){return false;} 46 bool partialLoad(in ubyte[] data, out ubyte[] rawImage){return false;} 47 bool loadTexture(ubyte[] rawImage){return false;} 48 override int getKerning(dchar current, dchar next) const{return 0;} 49 override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const{return 0;} 50 override HipFont getFontWithSize(uint size){return new HipNullFont();} 51 } 52 53 54 version(HipArsdFont) 55 /** 56 * Check the unicode table: https://unicode-table.com/en/blocks/ 57 * There is a lot of character ranges that defines a set of characters in a language, such as: 58 * 0000—007F Basic Latin 59 * 0080—00FF Latin-1 Supplement 60 * 0100—017F Latin Extended-A 61 * 0180—024F Latin Extended-B 62 * Maybe it will prove more useful than having a default charset 63 */ 64 class HipArsd_TTF_Font : HipFont 65 { 66 import arsd.ttf; 67 protected float fontScale; 68 protected TtfFont font; 69 string path; 70 protected uint fontSize = 32; 71 protected uint _textureWidth, _textureHeight; 72 protected Hip_TTF_Font mainInstance; 73 protected Hip_TTF_Font[] clones; 74 75 this(string path, uint fontSize = 32) 76 { 77 this.path = path; 78 this.fontSize = fontSize; 79 } 80 /** 81 * This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used 82 * If you don't care about async 83 */ 84 bool loadFromMemory(in ubyte[] data) 85 { 86 if(data == null || data.length == 0) 87 return false; 88 try{font = TtfFont(data);} 89 catch(Exception e){return false;} 90 return loadTexture( 91 generateImage(fontSize, _textureWidth, _textureHeight) 92 ); 93 } 94 95 bool partialLoad(in ubyte[] data, out ubyte[] rawImage) 96 { 97 font = TtfFont(data); 98 rawImage = generateImage(fontSize, _textureWidth, _textureHeight); 99 return true; 100 } 101 102 override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const 103 { 104 return cast(int)(fontScale*stbtt_GetGlyphKernAdvance(cast(stbtt_fontinfo*)&font.font, current.glyphIndex, next.glyphIndex)); 105 } 106 override int getKerning(dchar current, dchar next) const 107 { 108 return cast(int)(fontScale*stbtt_GetCodepointKernAdvance(cast(stbtt_fontinfo*)&font.font, int(current), int(next))); 109 } 110 111 112 bool loadTexture(ubyte[] rawImage) 113 { 114 assert(rawImage !is null, "Must first generate a texture before uploading to GPU"); 115 import hip.image; 116 import hip.hiprenderer.texture; 117 import hip.error.handler; 118 HipImageImpl img = new HipImageImpl(); 119 img.loadRaw(rawImage, _textureWidth, _textureHeight, 1); 120 HipTexture t = new HipTexture(); 121 122 bool ret = t.load(img); 123 ErrorHandler.assertErrorMessage(ret, "Loading TTF", "Could not create texture for TTF"); 124 texture = t; 125 return ret; 126 } 127 128 /** 129 * This function returns a new font using the same data file, with a new size. 130 * The font data will reference to this same one 131 */ 132 override HipFont getFontWithSize(uint size) 133 { 134 Hip_TTF_Font ret = new Hip_TTF_Font(this.path, size); 135 ret.font = cast(TtfFont)this.font; 136 ret.mainInstance = cast(Hip_TTF_Font)(mainInstance is null ? this : mainInstance); 137 if(mainInstance) 138 mainInstance.clones~= ret; 139 else 140 clones~= ret; 141 142 if(!ret.loadTexture(ret.generateImage(size, ret._textureWidth, ret._textureHeight))) 143 return null; 144 145 return cast(HipFont)ret; 146 } 147 148 protected RenderizedChar renderCharacter(dchar ch, int size, float shift_x = 0.0, float shift_y = 0.0) 149 { 150 RenderizedChar rch; 151 rch.ch = ch; 152 rch.data = font.renderCharacter(ch, size, rch.width, rch.height, shift_x, shift_y); 153 return rch; 154 } 155 /** 156 * I'm no good packer. The image will be at least 2048xMinPowOf2 157 */ 158 protected ubyte[] generateImage(int size, out uint width, out uint height, dstring charset = defaultCharset) 159 { 160 if(charset.length == 0) 161 return null; 162 scope RenderizedChar[] fontChars = new RenderizedChar[charset.length]; //TODO: USe that as it is more optimised 163 scope(exit) 164 { 165 foreach(ch; fontChars) 166 ch.dispose(); 167 import core.memory; 168 GC.free(fontChars.ptr); 169 } 170 171 uint avgWidth = 0; 172 uint avgHeight = 0; 173 size_t i = 0; 174 foreach(dc; charset) 175 { 176 RenderizedChar rc = renderCharacter(dc, size); 177 avgWidth+= rc.width; 178 avgHeight+= rc.height; 179 fontChars[i++] = rc; 180 } 181 //Add as an error (pixel bleeding) 182 avgWidth = cast(uint)(avgWidth / charset.length) + 2; 183 avgHeight = cast(uint)(avgHeight / charset.length) + 2; 184 import std.algorithm.sorting:sort; 185 enum hSpacing = 1; 186 enum vSpacing = 1; 187 float x = 1; 188 float y = 0; 189 float optY = 0; 190 float scale = stbtt_ScaleForPixelHeight(&font.font, size); 191 192 //Setting details 193 fontScale = scale; 194 195 int ascent, descent, lineGap; 196 stbtt_GetFontVMetrics(&font.font, &ascent, &descent, &lineGap); 197 198 lineBreakHeight = cast(uint)(int(ascent - descent + lineGap) * scale); 199 200 //First guarantee the big size 201 import core.math:sqrt; 202 uint sqrtOfCharset = cast(uint)sqrt(cast(float)charset.length) + 1; 203 uint imageWidth = avgWidth * sqrtOfCharset; 204 uint imageHeight = avgHeight * sqrtOfCharset; 205 imageHeight = nextPowerOfTwo(imageHeight); 206 imageWidth = nextPowerOfTwo(imageWidth); 207 208 width = imageWidth; 209 height = imageHeight; 210 211 ubyte[] image = new ubyte[](imageWidth*imageHeight); 212 213 int largestHeightInRow = 0; 214 foreach(fontCh; sort!"a.height > b.height"(fontChars)) 215 { 216 int g = stbtt_FindGlyphIndex(&font.font, fontCh.ch); 217 int xAdvance, xOffset, yOffset, lsb; 218 int x1, y1; 219 stbtt_GetGlyphHMetrics(&font.font, g, &xAdvance, &lsb); 220 stbtt_GetGlyphBitmapBox(&font.font, g, scale,scale, &xOffset,&yOffset,&x1,&y1); 221 if(fontCh.ch == ' ') 222 { 223 int space_x0, space_x1; 224 if(fontCh.width == 0) 225 { 226 stbtt_GetCodepointBitmapBox(&font.font, int('n'), scale,scale, &space_x0, null, &space_x1, null); 227 spaceWidth = space_x1 - space_x0; 228 } 229 else 230 spaceWidth = fontCh.width; 231 } 232 233 if(x + fontCh.width + hSpacing > imageWidth) 234 { 235 x = hSpacing; 236 y+= largestHeightInRow + vSpacing; 237 largestHeightInRow = 0; 238 } 239 240 241 characters[fontCh.ch] = HipFontChar(fontCh.ch, cast(int)x, cast(int)y, fontCh.width, fontCh.height, 242 243 xOffset, yOffset, round(xAdvance*scale), 0, 0, 244 cast(float)x/imageWidth, cast(float)y/imageHeight, 245 cast(float)fontCh.width/imageWidth, cast(float)fontCh.height/imageHeight, 246 g 247 ); 248 fontCh.blitToImage(image, cast(int)(x), cast(int)(y), imageWidth, imageHeight); 249 x+= fontCh.width + hSpacing; 250 251 if(fontCh.height > largestHeightInRow) 252 largestHeightInRow = fontCh.height; 253 } 254 return image; 255 } 256 257 } 258 259 version(HipNullFont) 260 alias Hip_TTF_Font = HipNullFont; 261 else version(HipArsdFont) 262 alias Hip_TTF_Font = HipArsd_TTF_Font; 263 264 265 private struct RenderizedChar 266 { 267 dchar ch; 268 int size; 269 int width; 270 int height; 271 272 ubyte[] data; 273 274 void blitToImage(ref ubyte[] texture, int startX, int startY, int textureWidth, int textureHeight) 275 { 276 assert(startX + width < textureWidth, "Out of X boundaries"); 277 for(size_t i = 0; i < height; i++) 278 { 279 size_t pos = (startY+i)*textureWidth + startX; 280 assert(startY + i < textureHeight, "Out of Y boundaries"); 281 texture[pos..pos+width] = data[i*width..(i+1)*width]; 282 } 283 } 284 285 void dispose() 286 { 287 version(HipArsdFont) 288 { 289 if(data.ptr != null) 290 { 291 import arsd.ttf; 292 stbtt_FreeBitmap(data.ptr, null); 293 } 294 } 295 data = null; 296 } 297 }